﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using ServiceStack.Configuration;
using ServiceStack.Host;
using ServiceStack.Text;
using ServiceStack.Web;

namespace ServiceStack.Auth
{
    //DigestAuth Info: http://www.ntu.edu.sg/home/ehchua/programming/webprogramming/HTTP_Authentication.html
    public class DigestAuthProvider : AuthProvider, IAuthWithRequest
    {
        public override string Type => "Digest";
        
        public static string Name = AuthenticateService.DigestProvider;
        public static string Realm = "/auth/" + AuthenticateService.DigestProvider;
        public static int NonceTimeOut = 600;
        public string PrivateKey;
        public IAppSettings AppSettings { get; set; }

        public DigestAuthProvider()
        {
            Provider = Name;
            PrivateKey = Guid.NewGuid().ToString();
            AuthRealm = Realm;
        }

        public DigestAuthProvider(IAppSettings appSettings, string authRealm, string authProvider)
            : base(appSettings, authRealm, authProvider) { }

        public DigestAuthProvider(IAppSettings appSettings)
            : base(appSettings, Realm, Name) { }

        public virtual bool TryAuthenticate(IServiceBase authService, string userName, string password)
        {
            var authRepo = HostContext.AppHost.GetAuthRepository(authService.Request);
            using (authRepo as IDisposable)
            {
                var session = authService.GetSession();
                var digestInfo = authService.Request.GetDigestAuth();
                if (authRepo.TryAuthenticate(digestInfo, PrivateKey, NonceTimeOut, session.Sequence, out var userAuth))
                {
                    session.Sequence = digestInfo["nc"];
                    session.PopulateSession(userAuth, authRepo);
                    return true;
                }
                return false;
            }
        }

        public override bool IsAuthorized(IAuthSession session, IAuthTokens tokens, Authenticate request = null)
        {
            if (request != null)
            {
                if (!LoginMatchesSession(session, request.UserName))
                {
                    return false;
                }
            }

            return session != null && session.IsAuthenticated && !session.UserAuthName.IsNullOrEmpty();
        }

        public override Task<object> AuthenticateAsync(IServiceBase authService, IAuthSession session, Authenticate request, CancellationToken token = default)
        {
            //new CredentialsAuthValidator().ValidateAndThrow(request);
            return AuthenticateAsync(authService, session, request.UserName, request.Password, token);
        }

        protected async Task<object> AuthenticateAsync(IServiceBase authService, IAuthSession session, string userName, string password, CancellationToken token=default)
        {
            if (!LoginMatchesSession(session, userName))
            {
                await authService.RemoveSessionAsync(token).ConfigAwait();
                session = await authService.GetSessionAsync(token: token).ConfigAwait();
            }

            if (TryAuthenticate(authService, userName, password))
            {
                session.IsAuthenticated = true;

                if (session.UserAuthName == null)
                    session.UserAuthName = userName;

                var response = await OnAuthenticatedAsync(authService, session, null, null, token).ConfigAwait();
                if (response != null)
                    return response;

                return new AuthenticateResponse
                {
                    UserId = session.UserAuthId,
                    UserName = userName,
                    SessionId = session.Id,
                };
            }

            throw HttpError.Unauthorized(ErrorMessages.InvalidUsernameOrPassword.Localize(authService.Request));
        }

        public override async Task<IHttpResult> OnAuthenticatedAsync(IServiceBase authService, IAuthSession session, IAuthTokens tokens, Dictionary<string, string> authInfo, CancellationToken token=default)
        {
            session.AuthProvider = Name;
            if (session is AuthUserSession userSession)
            {
                await LoadUserAuthInfoAsync(userSession, tokens, authInfo, token).ConfigAwait();
                HostContext.TryResolve<IAuthMetadataProvider>().SafeAddMetadata(tokens, authInfo);

                LoadUserAuthFilter?.Invoke(userSession, tokens, authInfo);
                if (LoadUserAuthInfoFilterAsync != null)
                    await LoadUserAuthInfoFilterAsync(userSession, tokens, authInfo, token);
            }

            if (session is IAuthSessionExtended authSession)
            {
                // ReSharper disable once MethodHasAsyncOverloadWithCancellation
                var failed = authSession.Validate(authService, session, tokens, authInfo)
                     ?? await authSession.ValidateAsync(authService, session, tokens, authInfo, token) 
                     ?? AuthEvents.Validate(authService, session, tokens, authInfo)
                     ?? (AuthEvents is IAuthEventsAsync asyncEvents 
                         ? await asyncEvents.ValidateAsync(authService, session, tokens, authInfo, token)
                         : null);
                if (failed != null)
                {
                    await authService.RemoveSessionAsync(token).ConfigAwait();
                    return failed;
                }
            }

            var authRepo = GetUserAuthRepositoryAsync(authService.Request);
            await using (authRepo as IAsyncDisposable)
            {
                if (authRepo != null)
                {
                    if (tokens != null)
                    {
                        authInfo.ForEach((x, y) => tokens.Items[x] = y);
                        session.UserAuthId = (await authRepo.CreateOrMergeAuthSessionAsync(session, tokens, token)).UserAuthId.ToString();
                    }

                    foreach (var oAuthToken in session.GetAuthTokens())
                    {
                        var authProvider = AuthenticateService.GetAuthProvider(oAuthToken.Provider);

                        var userAuthProvider = authProvider as OAuthProvider;
                        userAuthProvider?.LoadUserOAuthProvider(session, oAuthToken);
                    }

                    var failed = await ValidateAccountAsync(authService, authRepo, session, tokens, token).ConfigAwait();
                    if (failed != null)
                        return failed;
                }
            }

            try
            {
                session.IsAuthenticated = true;
                session.OnAuthenticated(authService, session, tokens, authInfo);
                if (session is IAuthSessionExtended sessionExt)
                    await sessionExt.OnAuthenticatedAsync(authService, session, tokens, authInfo, token).ConfigAwait();
                AuthEvents.OnAuthenticated(authService.Request, session, authService, tokens, authInfo);
                if (AuthEvents is IAuthEventsAsync asyncEvents)
                    await asyncEvents.OnAuthenticatedAsync(authService.Request, session, authService, tokens, authInfo, token).ConfigAwait();
            }
            finally
            {
                await this.SaveSessionAsync(authService, session, SessionExpiry, token).ConfigAwait();
                authService.Request.Items[Keywords.DidAuthenticate] = true;
            }

            return null;
        }

        public override Task OnFailedAuthentication(IAuthSession session, IRequest httpReq, IResponse httpRes)
        {
            var digestHelper = new DigestAuthFunctions();
            httpRes.StatusCode = (int)HttpStatusCode.Unauthorized;
            httpRes.AddHeader(HttpHeaders.WwwAuthenticate,
                $"{Provider} realm=\"{AuthRealm}\", nonce=\"{digestHelper.GetNonce(httpReq.UserHostAddress, PrivateKey)}\", qop=\"auth\"");
            return HostContext.AppHost.HandleShortCircuitedErrors(httpReq, httpRes, httpReq.Dto);
        }

        public async Task PreAuthenticateAsync(IRequest req, IResponse res)
        {
            var digestAuth = req.GetDigestAuth();
            if (digestAuth != null)
            {
                //Need to run SessionFeature filter since its not executed before this attribute (Priority -100)			
                SessionFeature.AddSessionIdToRequestFilter(req, res, null); //Required to get req.GetSessionId()

                using var authService = HostContext.ResolveService<AuthenticateService>(req);
                var response = await authService.PostAsync(new Authenticate
                {
                    provider = Name,
                    nonce = digestAuth["nonce"],
                    uri = digestAuth["uri"],
                    response = digestAuth["response"],
                    qop = digestAuth["qop"],
                    nc = digestAuth["nc"],
                    cnonce = digestAuth["cnonce"],
                    UserName = digestAuth["username"]
                }).ConfigAwait();
            }
        }
    }
}